﻿using UnityEngine;
using System.Collections;
using Assets.components;
using DiamondSquare;
using Assets.entities;
using FloodFill;
using System.Collections.Generic;
using System;

[RequireComponent(typeof(VoxelWorld))]
[RequireComponent(typeof(VoxelPrototypeFactory))]
[RequireComponent(typeof(DiamondSeeder))]
[RequireComponent(typeof(FrenzyIslandContentGenerator))]
[RequireComponent(typeof(FrenzyPeopleController))]
[RequireComponent(typeof(AstarPath))]
public class FrenzyIslandGenerator : MonoBehaviour {

    public struct VoxelInfo
    {
        public Voxel voxel;
        public Vector3 position;
    }

    [Range(0, 1.0f)]
    public float roughness = 0.2f;
    [Range(0, 1.0f)]
    public float waterThreshold = 0.2f;
    public int size = 128;
    public int maxHeight = 16;

    private DiamondMatrix _matrix;
    private float[,] _heightMap;
    private int[,] biomeMap;

    private FloodFiller floodFillerWater;
    private FloodFiller floodFillerField;
    public Scanner scannerWater;
    public Scanner scannerField;

    public List<VoxelInfo> ListSandy { get; set; }
    public List<VoxelInfo> ListGrassy { get; set; }


    void Start () {
        ListSandy = new List<VoxelInfo>();
        ListGrassy = new List<VoxelInfo>();
        this.createIsland();
	}

    public void createIsland(){
        DiamondSeeder matrixSeeder = this.GetComponent<DiamondSeeder>();
        VoxelWorld world = this.GetComponent<VoxelWorld>();
        float waterLevel = Mathf.Ceil(this.maxHeight * this.waterThreshold);
        
        //costruisco ed elaboro la diamond square matrix.
        this._matrix = new DiamondMatrix(this.size, this.roughness, matrixSeeder);
        this._matrix.Elaborate();
        this._heightMap = new float[this.size, this.size];
        this.biomeMap = new int[this.size, this.size];

        //popolo il voxel engine con le informazioni dettate dai punti di controllo
        //deducibili dalla diamond square matrix.
        this.buildWorldSurface(waterLevel);

        //dopo aver creato l'isola effettuo una scansione per trovare la lista dei vocel per tipo
        this.ScanIsland();
        
        //aggiungo le risorse al mondo di gioco.
        this.buildWorldResources(waterLevel);


        //la costruzione del mondo di gioco è completata;
        //tutti i voxel del mondo di gioco sono stati aggiunti al voxel engine e
        //tutti i gameObject degli elementi naturali sono stati aggiunti alla scena.
        //Lancio quindi la poligonizzazione complessiva del voxel engine, al termine
        //della quale verrà generato il grafo per il pathfinding.
        world.update(
            new VoxelWorld.OnConpleteUpdate(OnEndUpdate)
        );
    }

    private void ScanIsland()
    {
        //dopo aver creato l'isola, uso scanner per individuare la lista di spiaggia e terreno
        floodFillerField = new FloodFiller(this.biomeMap, this.size / 2, this.size / 2);
        floodFillerWater = new FloodFiller(this.biomeMap, 0, 0);
        scannerField = new Scanner(floodFillerField);
        scannerWater = new Scanner(floodFillerWater);

        //prende i componenti
        VoxelPrototypeFactory prototypeFactory = GetComponent<VoxelPrototypeFactory>();

        //dichiaro i costruttori (stesso indice contenuto nell'inspector)
        VoxelPrototype grassyPrototype = prototypeFactory.getPrototype(0);
        VoxelPrototype muddyPrototype = prototypeFactory.getPrototype(1);
        VoxelPrototype sandyPrototype = prototypeFactory.getPrototype(2);
        VoxelPrototype waterPrototype = prototypeFactory.getPrototype(3);

        //inizializzo le liste dei materiali
        foreach (Node item in scannerField.FloodResult.ListVisited)
        {
            FrenzyIslandGenerator.VoxelInfo voxelInfo = new FrenzyIslandGenerator.VoxelInfo()
            {
                voxel = getTopVoxelAt(item.x, item.z),
                position = new Vector3(item.x, getHeightVoxelAt(item.x, item.z), item.z)
            };

            if (voxelInfo.voxel.prototype.subMaterialIndex == sandyPrototype.subMaterialIndex)
            {
                ListSandy.Add(voxelInfo);
            }

            if (voxelInfo.voxel.prototype.subMaterialIndex == grassyPrototype.subMaterialIndex)
            {
                ListGrassy.Add(voxelInfo);
            }
        }
    }

    private void buildWorldSurface(float waterLevel) {
        VoxelWorld world = this.GetComponent<VoxelWorld>();
        VoxelPrototypeFactory factory = this.GetComponent<VoxelPrototypeFactory>();
        VoxelPrototype grassyPrototype = factory.getPrototype(0);
        VoxelPrototype muddyPrototype = factory.getPrototype(1);
        VoxelPrototype sandyPrototype = factory.getPrototype(2);
        VoxelPrototype waterPrototype = factory.getPrototype(3);

        //itero sui punti di controllo dello spazio tridimensionale
        //rappresentati dal contenuto bidimensionale della diamond square matrix.
        MarchingCube filledCube = MarchingCube.full;
        for (int x = 1; x < this._matrix.Size-2; x++)
        {
            for (int z = 1; z < this._matrix.Size-2; z++)
            {
                int yMax = this.getNormalizedValueAt(x, z);
                int xCoord = x - 1;
                int zCoord = this._matrix.Size - z - 1;

                //andrebbe limitato, questo for, per la massima altezza
                int y = 0;
                for (y = 0; y <= this.maxHeight; y++) {
                    MarchingCube cube = this.getMarchingCube(x, y, z);
                    MarchingCube currentCube;
                    VoxelPrototype currentPrototype = null;
                    Voxel currentVoxel = null;
                    int yCoord = y;
                    
                    //scelta del blocco
                    if (y <= waterLevel && y <= yMax) {
                        //altezza minore del livello dell'acqua e minore dell'altezza massima per questa colonna
                        //blocco di sabbia
                        currentPrototype = sandyPrototype;
                        currentCube = filledCube;
                        currentVoxel = currentPrototype.instantiateVoxel(new VoxelData(currentCube, null));

                        //non è acqua
                        biomeMap[xCoord, zCoord] = 1;

                    } else if (y <= waterLevel && y > yMax) {
                        //altezza minore del livello dell'acqua ma maggiore dell'altezza massima per questa colonna
                        //blocco d'acqua
                        currentPrototype = waterPrototype;
                        currentCube = filledCube;
                        currentVoxel = currentPrototype.instantiateVoxel(new VoxelData(currentCube, null));

                        //acqua
                        biomeMap[xCoord, zCoord] = 0;

                    }
                    else if (y > waterLevel)
                    {
                        //se l'altezza è superiore al livello dell'acqua e non si sta
                        //cercando di disegnare un blocco d'aria (cioè vuoto) allora
                        //si sceglie il blocco erboso soltanto se sto disegnando un
                        //blocco di isosuperficie oppure il più alto blocco pieno.
                        //Viceversa il blocco è di terra.
                        if (!cube.isEmpty) {
                            if (cube.isIsosurface || (!cube.isIsosurface && y == yMax-1)) {
                                currentPrototype = grassyPrototype;
                                currentCube = cube;
                            } else {
                                currentPrototype = muddyPrototype;
                                currentCube = cube;
                            }
                            currentVoxel = currentPrototype.instantiateVoxel(new VoxelData(currentCube, null));

                            //non è acqua
                            biomeMap[xCoord, zCoord] = 1;

                        } else {
                            //al primo blocco vuoto in altezza imposto il valore nella height map ed esco
                            break;
                        }
                    }
                    
                    if (currentVoxel != null) {
                        world.setVoxelAt(currentVoxel, xCoord, yCoord, zCoord);
                    }
                }

                //if (y - 1 > waterLevel &&  sandyPrototype)
                //    biomeMap[xCoord, zCoord] = 1;
                //else
                //    biomeMap[xCoord, zCoord] = 0;

                this._heightMap[xCoord, zCoord] = y;

            }
        }
    }

    private void buildWorldResources(float waterLevel) {
        FrenzyIslandContentGenerator contentGenerator = this.GetComponent<FrenzyIslandContentGenerator>();
        VoxelPrototypeFactory prototypeFactory = this.GetComponent<VoxelPrototypeFactory>();
        VoxelWorld world = this.GetComponent<VoxelWorld>();
        VoxelPrototype prototype = prototypeFactory.getPrototype(0);
        FrenzyPeopleController peopleGenerator = this.GetComponent<FrenzyPeopleController>();
        
        //spamma le risorse e la popolazione
        contentGenerator.GenerateContent();
        peopleGenerator.GeneratePeople();
    }

    private void OnEndUpdate(){
        //effettuo la prima scansione contenente voxel e contenuti
        if ( AstarPath.active != null && AstarPath.active.isScanning == false)
            AstarPath.active.Scan();
    }

    /// <summary>
    ///     Restituisce il voxel superiore
    ///     N.B.
    ///     Applicata la correzione di montrone perchè _heightMap restituisce il voxel 
    ///     immediatamenre superiore
    /// </summary>
    /// <param name="x"></param>
    /// <param name="z"></param>
    /// <returns></returns>
    public Voxel getTopVoxelAt(int x, int z) {
        VoxelWorld world = this.GetComponent<VoxelWorld>();
        int y = Mathf.CeilToInt(this._heightMap[x, z]) - 1;
        return world.getVoxelAt(x, y, z);
    }

    public int getHeightVoxelAt(int x, int z)
    {
        int y = -1;
        if (x < 0 || x > this._heightMap.GetLength(0)) return -1;
        if (z < 0 || z > this._heightMap.GetLength(1)) return -1;

        VoxelWorld world = this.GetComponent<VoxelWorld>();
        try
        {
            y = Mathf.CeilToInt(this._heightMap[x, z]) - 1;
        }catch(Exception e)
        {
            Debug.LogError("Errore getHeightVoxelAt at x=" + x + " z=" + z);
            return -1;
        }
        
        return y;
    }

    /// <summary>
    /// Restituisce il marching cube associato alle coordinate stabilite.
    /// le coordinate x, y e z costituiscono il vertice posteriore sinistro
    /// sulla facciata inferiore del cubo
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="z"></param>
    /// <returns></returns>
    private MarchingCube getMarchingCube(int x, int y, int z) {
        int yMax1 = this.getNormalizedValueAt(x, z);
        int yMax2 = this.getNormalizedValueAt(x+1, z);
        int yMax3 = this.getNormalizedValueAt(x+1, z+1);
        int yMax4 = this.getNormalizedValueAt(x, z+1);

        bool y1 = (yMax1 - y) > 0;
        bool y2 = (yMax2 - y) > 0;
        bool y3 = (yMax3 - y) > 0;
        bool y4 = (yMax4 - y) > 0;
        bool y5 = (yMax1 - y + 1) > 0;
        bool y6 = (yMax2 - y + 1) > 0;
        bool y7 = (yMax3 - y + 1) > 0;
        bool y8 = (yMax4 - y + 1) > 0;
        
        return new MarchingCube(y1, y2, y3, y4, y5, y6, y7, y8);
    }

    /// <summary>
    /// Restituisce il valore normalizzato, alle posizioni x e y, della diamond square.
    /// La normalizzazione moltiplica il valore per il fattore di altezza e lo trasla di 1.
    /// </summary>
    /// <param name="x"></param>
    /// <param name="z"></param>
    /// <returns>il valore normalizzato</returns>
    private int getNormalizedValueAt(int x, int z) {
        int xCoord = z;
        int zCoord = x;

        float value = (float)this._matrix.GetValue(xCoord, zCoord);
        return Mathf.CeilToInt(value * this.maxHeight) + 1;
    }

    public Scanner ScannerField { 
        get { 
            return scannerField; 
        } 
    }

    public Scanner ScannerWater { 
        get { 
            return scannerWater; 
        } 
    }

    public void OnDrawGizmos()
    {
        #region Decomentare per vedere come lavora il floodfill
        //Gizmos.color = Color.blue;
        //if (floodFillerWater != null && floodFillerWater.ListEntryPoint != null)
        //foreach (Node item in floodFillerWater.ListEntryPoint)
        //{
        //    Gizmos.DrawLine(
        //        new Vector3(item.x, 0, item.z), 
        //        new Vector3(item.x, 20, item.z));
        //}


        //if (floodFillerField != null && floodFillerField.ListEntryPoint != null)
        //{
        //    Gizmos.color = Color.green;
        //    foreach (Node item in floodFillerField.ListEntryPoint)
        //    {
        //        Gizmos.DrawLine(
        //            new Vector3(item.x, 0, item.z),
        //            new Vector3(item.x, 20, item.z)
        //            );
        //    }

        //    Gizmos.color = Color.red;
        //    foreach (Node item in floodFillerField.ListVisited)
        //    {
        //        Gizmos.DrawLine(
        //            new Vector3(item.x, 0, item.z),
        //            new Vector3(item.x, 20, item.z)
        //            );
        //    }
        //}
        #endregion

        #region Scanner

        //Gizmos.color = Color.green;
        //foreach (Vector3 item in scannerField.MainIsland.ListVertex)
        //{
        //    Gizmos.DrawLine(
        //        new Vector3(item.x, 0, item.z),
        //        new Vector3(item.x, 20, item.z));
        //}

        #endregion

    }

}